iT邦幫忙

2023 iThome 鐵人賽

DAY 22
0
Vue.js

用 Nuxt Content 搭配 Obsidian 建立自己的 Digital Garden系列 第 22

透過外掛實作將 Wikilink 轉換成 Markdown Link

  • 分享至 

  • xImage
  •  

Obsidian 最大的賣點之一就是雙向連結,而這個功能特別仰賴 [[note-name]] 這樣的格式,讓我們能夠快速輸入。但在〈如何將 Obsidian 的附件跟著 Nuxt Content 發布〉時,因為 Nuxt Content 使用的 MDC 並沒有支援這樣的語法,所以只能先將其停用,今天就來實作吧。

今天所有實作的程式碼都會放在 @/server/plugins/wikilink.ts 裡,但為了方便解釋,所以會將程式碼分段去解說。

首先建立一篇文章用來證明我們渲染的成果是否符合預期,這邊是建立 @/content/sandbox/wikilink.md

---
title: Wiki Link 渲染示例
created_at: 2023-10-07T23:22:42.800+08:00
published_at: 2023-10-07T23:22:56.834+08:00
tags: []
---
## Text Link Testing:

### Regular Case

```
- Expected:
    - single `[]`: [/dev/testcases/note]
    - double `[]`: [note](/dev/testcases/note)
    - double `[]` with alias: [My Note](/dev/testcases/note)
- Actual:
    - single `[]`: [/dev/testcases/note]
    - double `[]`: [[/dev/testcases/note]]
    - double `[]` with alias: [[/dev/testcases/note|My Note]]
```

- Expected:
    - single `[]`: [/dev/testcases/note]
    - double `[]`: [note](/dev/testcases/note)
    - double `[]` with alias: [My Note](/dev/testcases/note)
- Actual:
    - single `[]`: [/dev/testcases/note]
    - double `[]`: [[/dev/testcases/note]]
    - double `[]` with alias: [[/dev/testcases/note|My Note]]

### Special Case Testing

#### Number

```
- Expected: 
    - [100](</dev/testcases/100>)
- Actually: 
    - [[/dev/testcases/100|100]]
```

- Expected: 
    - [100](</dev/testcases/100>)
- Actually: 
    - [[/dev/testcases/100|100]]
 

#### Han

```
- Expected: 
    - [測試](</dev/testcases/%E6%B8%AC%E8%A9%A6>)
    - [測試](</dev/testcases/測試>)
- Actually: 
    - [[/dev/testcases/%E6%B8%AC%E8%A9%A6|測試]]
    - [[/dev/testcases/測試|測試]]
```

- Expected: 
    - [測試](</dev/testcases/%E6%B8%AC%E8%A9%A6>)
    - [測試](</dev/testcases/測試>)
- Actually: 
    - [[/dev/testcases/%E6%B8%AC%E8%A9%A6|測試]]
    - [[/dev/testcases/測試|測試]]


#### Symbol

```
- Expected:
    - dot: [test.with.space](</dev/testcases/test.with.space>)
    - dash: [test-with-space](</dev/testcases/test-with-space>)
    - underscore: [test_with_space](</dev/testcases/test_with_space>)
- Actually:
    - dot: [[/dev/testcases/test.with.space|test.with.space]]
    - dash: [[/dev/testcases/test-with-space|test-with-space]]
    - underscore: [[/dev/testcases/test_with_space|test_with_space]]
```

- Expected:
    - dot: [test.with.space](</dev/testcases/test.with.space>)
    - dash: [test-with-space](</dev/testcases/test-with-space>)
    - underscore: [test_with_space](</dev/testcases/test_with_space>)
- Actually:
    - dot: [[/dev/testcases/test.with.space|test.with.space]]
    - dash: [[/dev/testcases/test-with-space|test-with-space]]
    - underscore: [[/dev/testcases/test_with_space|test_with_space]]

#### Space with URL encoding
```
- Expected:
    - Space: [test with space](</dev/testcases/test%20with%20space>)
- Actually:
    - Space: [[/dev/testcases/test%20with%20space|test with space]]
```

- Expected:
    - Space: [test with space](</dev/testcases/test%20with%20space>)
- Actually:
    - Space: [[/dev/testcases/test%20with%20space|test with space]]

#### Url enclosed in angle brackets
```
- Expected: [angle](</dev/testcases/angle>)
- Actually: [[</dev/testcases/angle>|angle]]
```
 
- Expected: [angle](</dev/testcases/angle>)
- Actually: [[</dev/testcases/angle>|angle]]

#### Han
```
- Expected:
    - Space: [測試](</dev/testcases/測試>)
- Actually:
    - Space: [[/dev/testcases/測試|測試]]
```

- Expected:
    - Space: [測試](</dev/testcases/測試>)
- Actually:
    - Space: [[/dev/testcases/測試|測試]]

#### Mixed
```
- Expected:
    - [測-試_mix.ed 的.情%20況](</dev/testcases/測-試_mix.ed 的.情%20況>)
- Actually
    - [[/dev/testcases/測-試_mix.ed 的.情%20況|測-試_mix.ed 的.情%20況]]
```

- Expected:
    - [測-試_mix.ed 的.情%20況](</dev/testcases/測-試_mix.ed 的.情%20況>)
- Actually
    - [[/dev/testcases/測-試_mix.ed 的.情%20況|測-試_mix.ed 的.情%20況]]

接著開始編寫程式碼

首先定義一個 Nitro Plugin,並建立一個 Hook,透過 'content:file:beforeParse' 這個 Hook,會讓內容檔案在被程式去解析前執行我們下方的函式。在這邊我們先做一個檢查,只要是 .md 結尾的檔案,其內文都會被 convertWikiLink() 轉換。

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('content:file:beforeParse', (file) => {
    if (file._id.endsWith('.md')) {
      file.body = convertWikiLink(file.body)
    }
  })
})

接著實作 convertWikiLink(),這邊有個特別注意的是,就是透過 isInCodeBlock 變數來判斷我們現在是不是在 code block,如果是的話,就不會做任何事,如果不是,就會將那行透過 convertLinkMarkdown() 進行轉換。

function convertWikiLink(text: string): string {
  let isInCodeBlock = false
  const convertedLines = text.split('\n').map((line) => {
    const isCodeBlockSyntax = line.startsWith('```') || line.startsWith('~~~')
    isInCodeBlock = isCodeBlockSyntax ? !isInCodeBlock : isInCodeBlock

    if (!isInCodeBlock) {
      line = convertLinkMarkdown(line)
    }
    return line
  })

  return convertedLines.join('\n')
}

接著在 convertLinkMarkdown() 中,首先會透過 generateWikiLinkRegExp() 去取得我們要使用的正則表達式,然後開始去轉換,其中正則表達式會擷取兩個變數,一個是聯結路徑,一個是別名,透過這個方式協助我們重組為 Markdown 連結。

function convertLinkMarkdown(line: string) {
  const regExp = generateWikiLinkRegExp()
  return line.replaceAll(regExp, (_, linkPath, linkAlias) => {
    const isExist = linkPath.startsWith('/')
    const filename = linkPath.split('/').pop()
    const unExistNoteLink = linkAlias || linkPath
    const linkMarkdown = `[${linkAlias || filename}](<${encondingNoneAlphabetUrl(linkPath)}>)`
    return isExist ? linkMarkdown : unExistNoteLink
  })
}

generateWikiLinkRegExp() 中,會將中日韓英四國文字以及連結會用到的百分比、空白都涵蓋在判斷的範圍。

const generateWikiLinkRegExp = function () {
  const regExpSets: RegExp[]  = [
    /\u4E00-\u9FFF/, // han: 中文漢字的 Unicode 範圍
    /\u3400-\u4DBF/, // hanExtend: 中文擴展 A 的 Unicode 範圍
    /\u3040-\u30FF/, // jpKana: 日文平假名和片假名的 Unicode 範圍
    /\uAC00-\uD7AF/, // krHangul: 韓文字母的 Unicode 範圍
    /\w\-./,         // enCommon: 所有的英文字母、數字、底線字元、破折號、小數點
    /%\\\//,         // symbol: 百分比字元、斜線與反斜線字元。
    /\s/,            // space: 空白字元(如空格和 Tab 字元)
    /<>/,            // angle: 角括號
  ]

  const pathSets = regExpSets.map(reg => reg.source).join('')
  const pathPattern = `[${pathSets}]+`
  const aliasPattern = /[^\[\]]+/.source

  const re = `\\[\\[\\<?(${pathPattern})\\>?(?:\\|(${aliasPattern}))?\\]\\]`
  return new RegExp(re, 'g')
}

如此就實作完成了,成果如下圖:


上一篇
為文章加上綱要
下一篇
透過外掛實作文章自訂網址功能
系列文
用 Nuxt Content 搭配 Obsidian 建立自己的 Digital Garden30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言